package checker

import (
	"kumachan/interpreter/lang/ast"
	"kumachan/interpreter/lang/common/attr"
	"kumachan/interpreter/lang/common/limits"
	"kumachan/interpreter/lang/common/source"
	"kumachan/interpreter/compiler/checker/typsys"
	"kumachan/interpreter/compiler/checker/checked"
)


type ProductPatternMatching func
	(in typsys.Type, mod string, lm LocalBindingMap) (
	checked.ProductPatternInfo, *source.Error)

func productPatternMatch(pattern ast.VariousPattern) ProductPatternMatching {
	switch P := pattern.Pattern.(type) {
	case ast.PatternTrivial:
		return patternMatchTrivial(P)
	case ast.PatternTuple:
		return patternMatchTuple(P)
	case ast.PatternRecord:
		return patternMatchRecord(P)
	default:
		panic("impossible branch")
	}
}

func patternMatchTrivial(pattern ast.PatternTrivial) ProductPatternMatching {
	return ProductPatternMatching(func(in typsys.Type, mod string, lm LocalBindingMap) (checked.ProductPatternInfo, *source.Error) {
		var n = ast.Id2String(pattern.Name)
		if n == Discarded {
			return checked.ProductPatternInfo(nil), nil
		}
		in = unboxWeak(in, mod)
		var binding = &checked.LocalBinding {
			Name:     n,
			Type:     in,
			Location: pattern.Location,
		}
		lm.add(binding)
		return checked.ProductPatternInfo([] checked.ProductPatternItemInfo { {
			Binding: binding,
			Index1:  0,
		}}), nil
	})
}

func patternMatchTuple(pattern ast.PatternTuple) ProductPatternMatching {
	return ProductPatternMatching(func(in typsys.Type, mod string, lm LocalBindingMap) (checked.ProductPatternInfo, *source.Error) {
		var L_required = len(pattern.Names)
		if L_required > limits.MaxProductPatternArity {
			return nil, source.MakeError(pattern.Location,
				E_PatternSizeLimitExceeded {})
		}
		if L_required == 0 {
			var ok = unboxUnit(in, mod)
			if !(ok) {
				return nil, source.MakeError(pattern.Location,
					E_CannotMatchUnit {
						TypeName: typsys.DescribeType(in, nil),
					})
			}
			return checked.ProductPatternInfo(nil), nil
		} else if L_required == 1 {
			var trivial = ast.PatternTrivial {
				Node: pattern.Node,
				Name: pattern.Names[0],
			}
			return patternMatchTrivial(trivial)(in, mod, lm)
		}
		var tuple, ok = unboxTuple(in, mod)
		if !(ok) {
			return nil, source.MakeError(pattern.Location,
				E_CannotMatchTuple {
					TypeName: typsys.DescribeType(in, nil),
				})
		}
		var L = len(tuple.Elements)
		if L != L_required {
			return nil, source.MakeError(pattern.Location,
				E_TupleSizeNotMatching {
					Required: uint(L_required),
					Given:    uint(L),
				})
		}
		var occurred = make(map[string] struct{})
		var info = make(checked.ProductPatternInfo, 0)
		for i := 0; i < L; i += 1 {
			var id = pattern.Names[i]
			var loc = id.Location
			var name = ast.Id2String(id)
			if name == Discarded {
				continue
			}
			var _, exists = occurred[name]
			occurred[name] = struct{}{}
			if exists {
				return nil, source.MakeError(loc,
					E_DuplicateBinding {
						BindingName: name,
					})
			}
			var t = tuple.Elements[i]
			var binding = &checked.LocalBinding {
				Name:     name,
				Type:     t,
				Location: loc,
			}
			info = append(info, checked.ProductPatternItemInfo {
				Binding: binding,
				Index1:  uint(i + 1),
			})
		}
		for _, item := range info {
			lm.add(item.Binding)
		}
		return info, nil
	})
}

func patternMatchRecord(pattern ast.PatternRecord) ProductPatternMatching {
	return ProductPatternMatching(func(in typsys.Type, mod string, lm LocalBindingMap) (checked.ProductPatternInfo, *source.Error) {
		if len(pattern.FieldMaps) > limits.MaxProductPatternArity {
			return nil, source.MakeError(pattern.Location,
				E_PatternSizeLimitExceeded {})
		}
		var record, ok = unboxRecord(in, mod)
		if !(ok) {
			return nil, source.MakeError(pattern.Location,
				E_CannotMatchRecord {
					TypeName: typsys.DescribeType(in, nil),
				})
		}
		var occurred = make(map[string] struct{})
		var info = make(checked.ProductPatternInfo, len(pattern.FieldMaps))
		for i, m := range pattern.FieldMaps {
			var binding_name = ast.Id2String(m.ValueName)
			var binding_loc = m.ValueName.Location
			var field_name = ast.Id2String(m.FieldName)
			var field_loc = m.FieldName.Location
			var _, binding_exists = occurred[binding_name]
			occurred[binding_name] = struct{}{}
			if binding_exists {
				return nil, source.MakeError(binding_loc,
					E_DuplicateBinding {
						BindingName: binding_name,
					})
			}
			var field_index, field_exists = record.FieldIndexMap[field_name]
			if field_exists {
				var field = record.Fields[field_index]
				var t = field.Type
				var binding = &checked.LocalBinding {
					Name:     binding_name,
					Type:     t,
					Location: binding_loc,
				}
				info[i] = checked.ProductPatternItemInfo {
					Binding: binding,
					Index1:  (1 + field_index),
				}
			} else {
				return nil, source.MakeError(field_loc,
					E_FieldNotFound {
						FieldName: field_name,
						TypeName:  typsys.DescribeType(in, nil),
					})
			}
		}
		for _, item := range info {
			lm.add(item.Binding)
		}
		return info, nil
	})
}

func checkGet(arg *checked.Expr, key ast.Identifier, loc source.Location) ExprChecker {
	return makeExprChecker(loc, func(cc *checkContext) checkResult {
		var key_str = ast.Id2String(key)
		var record, is_record = cc.unboxRecord(arg)
		if is_record {
			var field_name = key_str
			var field_index, exists = record.FieldIndexMap[field_name]
			if !(exists) {
				return cc.propagate(source.MakeError(key.Location,
					E_FieldNotFound {
						FieldName: field_name,
						TypeName:  cc.describeTypeOf(arg),
					}))
			}
			var field_type = record.Fields[field_index].Type
			return cc.assign(
				field_type,
				checked.TupleGet {
					Tuple: arg,
					Index: field_index,
				})
		} else {
			var m, ok = cc.resolveMethod(arg.Type, key_str)
			if ok {
				switch M := m.(type) {
				case concreteTypeMethods:
					var arg_node = ast.WrapTermAsExpr(MakeCheckedExprTerm(arg))
					var pivot = arg.Type
					return callFuncRefs(cc, M.FuncRefs, nil, arg_node, pivot)
				case interfaceTypeMethod:
					return cc.assign(
						M.Type,
						checked.InterfaceMethod {
							Interface: arg,
							Index:     M.Index,
						})
				default:
					panic("impossible branch")
				}
			} else {
				return cc.propagate(source.MakeError(key.Location,
					E_NoSuchFieldOrMethod {
						FieldName: key_str,
						TypeName:  cc.describeTypeOf(arg),
					}))
			}
		}
	})
}

func checkTuple(T ast.Tuple) ExprChecker {
	return makeExprChecker(T.Location, func(cc *checkContext) checkResult {
		var L = uint(len(T.Elements))
		if L == 0 {
			if T.Parallel { return cc.error(E_TupleUnexpectedParallelMark {}) }
			return cc.assign(typsys.UnitType {}, checked.UnitValue {})
		}
		if L == 1 {
			if T.Parallel { return cc.error(E_TupleUnexpectedParallelMark {}) }
			return cc.forwardToChildExpr(T.Elements[0])
		}
		if L > limits.MaxTupleSize {
			return cc.error(
				E_TooManyTupleElements { SizeLimitError {
					Given: uint(L),
					Limit: limits.MaxTupleSize,
				}})
		}
		var tuple, is_tuple = cc.getExpectedTuple(cc.expected)
		if is_tuple {
			var L_required = uint(len(tuple.Elements))
			if L != L_required {
				return cc.throw(source.MakeError(cc.location,
					E_TupleSizeNotMatching {
						Required: L_required,
						Given:    L,
					}))
			}
			var elements = make([] *checked.Expr, L)
			var types = make([] typsys.Type, L)
			for i := uint(0); i < L; i += 1 {
				var el, err = cc.checkChildExpr(tuple.Elements[i], T.Elements[i])
				if err != nil { return cc.throw(err) }
				elements[i] = el
				types[i] = el.Type
			}
			var tuple_expr_t = &typsys.NestedType { Content:
				typsys.Tuple { Elements:
					types,
				} }
			return cc.assign(
				tuple_expr_t,
				checked.Tuple {
					Parallel: T.Parallel,
					Elements: elements,
				})
		} else {
			var elements = make([] *checked.Expr, L)
			var types = make([] typsys.Type, L)
			for i := uint(0); i < L; i += 1 {
				var el, err = cc.checkChildExpr(nil, T.Elements[i])
				if err != nil { return cc.propagate(err) }
				elements[i] = el
				types[i] = el.Type
			}
			var tuple_expr_t = &typsys.NestedType { Content:
				typsys.Tuple { Elements:
					types,
				} }
			return cc.assign(
				tuple_expr_t,
				checked.Tuple {
					Parallel: T.Parallel,
					Elements: elements,
				})
		}
	})
}

func checkRecord(R ast.Record) ExprChecker {
	return makeExprChecker(R.Location, func(cc *checkContext) checkResult {
		{ var L = uint(len(R.Values))
		if L > limits.MaxRecordSize {
			return cc.error(
				E_TooManyRecordFields { SizeLimitError {
					Given: L,
					Limit: limits.MaxRecordSize,
				}})
		} }
		var update, has_update = R.Update.(ast.Update)
		var record, is_record = cc.getExpectedRecord(cc.expected)
		if !(has_update) && is_record {
			var mapping = make(map[string] uint)
			var fields = make([] typsys.Field, len(record.Fields))
			var values = make([] *checked.Expr, len(record.Fields))
			for _, item := range R.Values {
				var n = ast.Id2String(item.Key)
				var index_in_rec, exists = record.FieldIndexMap[n]
				if !(exists) {
					return cc.throw(source.MakeError(item.Key.Location,
						E_FieldNotFound {
							FieldName: n,
							TypeName:  cc.describeType(cc.expected),
						}))
				}
				var _, duplicate = mapping[n]
				mapping[n] = index_in_rec
				if duplicate {
					return cc.propagate(source.MakeError(item.Key.Location,
						E_DuplicateField {
							FieldName: n,
						}))
				}
				var field = record.Fields[index_in_rec]
				var value_node = getFieldValue(item)
				var expr, err = cc.checkChildExpr(field.Type, value_node)
				if err != nil { return cc.throw(err) }
				values[index_in_rec] = expr
				fields[index_in_rec] = typsys.Field {
					Attr: field.Attr,
					Name: n,
					Type: expr.Type,
				}
			}
			for i, field := range record.Fields {
				var _, provided = mapping[field.Name]
				if !(provided) {
					var t, is_certain = cc.getCertainType(field.Type)
					if is_certain {
						var ref = checkName(F_Default, t, cc.location)
						var expr, err = cc.checkChild(t, ref)
						if err == nil {
							var n = field.Name
							var index = uint(i)
							mapping[n] = index
							values[index] = expr
							fields[index] = typsys.Field {
								Attr: field.Attr,
								Name: n,
								Type: t,
							}
							continue
						} else {
							goto missing_field
						}
					} else {
						goto missing_field
					}
					missing_field:
					return cc.throw(source.MakeError(cc.location,
						E_MissingField {
							FieldName: field.Name,
						}))
				}
			}
			if len(mapping) != len(record.Fields) {
				panic("something went wrong")
			}
			var record_t = &typsys.NestedType {
				Content: typsys.Record {
					FieldIndexMap: mapping,
					Fields:        fields,
				},
			}
			return cc.assign(
				record_t,
				checked.Tuple {
					Elements: values,
				})
		}
		var L = uint(len(R.Values))
		var mapping = make(map[string] uint)
		var fields = make([] typsys.Field, L)
		var values = make([] *checked.Expr, L)
		for i, item := range R.Values {
			var key = item.Key
			var k = ast.Id2String(key)
			var _, duplicate = mapping[k]
			mapping[k] = uint(i)
			if duplicate {
				return cc.propagate(source.MakeError(item.Key.Location,
					E_DuplicateField {
						FieldName: k,
					}))
			}
			var value_node = getFieldValue(item)
			var value_expr, err = cc.checkChildExpr(nil, value_node)
			if err != nil { return cc.propagate(err) }
			values[i] = value_expr
			fields[i] = typsys.Field {
				Attr: attr.FieldAttrs {
					Attrs: attr.Attrs { Location: item.Location },
				},
				Name: k,
				Type: value_expr.Type,
			}
		}
		if has_update {
			var base, err = cc.checkChildExpr(nil, update.Base)
			if err != nil { return cc.propagate(err) }
			var base_record, ok = cc.unboxRecord(base)
			if !(ok) {
				return cc.propagate(source.MakeError(update.Base.Location,
					E_UpdateOnNonRecord {
						TypeName: cc.describeTypeOf(base),
					}))
			}
			var replaced = make([] checked.TupleUpdateElement, L)
			for i := uint(0); i < L; i += 1 {
				var field = fields[i]
				var k = field.Name
				var base_index, exists = base_record.FieldIndexMap[k]
				if !(exists) {
					return cc.error(
						E_FieldNotFound {
							FieldName: k,
							TypeName:  cc.describeTypeOf(base),
						})
				}
				var base_field = base_record.Fields[base_index]
				var err = cc.assignType(base_field.Type, field.Type)
				if err != nil { return cc.propagate(err) }
				replaced[i] = checked.TupleUpdateElement {
					Index: base_index,
					Value: values[i],
				}
			}
			var record_t = &typsys.NestedType { Content: base_record }
			return cc.assign(
				record_t,
				checked.TupleUpdate {
					Tuple:    base,
					Replaced: replaced,
				})
		} else {
			var record_t = &typsys.NestedType {
				Content: typsys.Record {
					FieldIndexMap: mapping,
					Fields:        fields,
				},
			}
			return cc.assign(
				record_t,
				checked.Tuple {
					Elements: values,
				})
		}
	})
}

func getFieldValue(item ast.FieldValue) ast.Expr {
	var given_value, given = item.Value.(ast.Expr)
	if given {
		return given_value
	} else {
		return desugarOmittedFieldValue(item.Key)
	}
}
func desugarOmittedFieldValue(key ast.Identifier) ast.Expr {
	return ast.Expr {
		Node: key.Node,
		Term: ast.VariousTerm {
			Node: key.Node,
			Term: ast.InlineRef {
				Node: key.Node,
				Item: key,
			},
		},
	}
}
func getTuple(t typsys.Type) (typsys.Tuple, bool) {
	var nested, is_nested = t.(*typsys.NestedType)
	if !(is_nested) { return typsys.Tuple {}, false }
	var tuple, is_tuple = nested.Content.(typsys.Tuple)
	return tuple, is_tuple
}
func getRecord(t typsys.Type) (typsys.Record, bool) {
	var nested, is_nested = t.(*typsys.NestedType)
	if !(is_nested) { return typsys.Record {}, false }
	var record, is_tuple = nested.Content.(typsys.Record)
	return record, is_tuple
}
func unboxWeak(t typsys.Type, mod string) typsys.Type {
	var inner, box, exists = typsys.Unbox(t, mod)
	if exists && box.WeakWrapping {
		return unboxWeak(inner, mod)
	} else {
		return t
	}
}
func getUnit(t typsys.Type) bool {
	var _, is_unit = t.(typsys.UnitType)
	return is_unit
}
func unboxUnit(t typsys.Type, mod string) bool {
	if getUnit(t) {
		return true
	} else {
		var inner, _, exists = typsys.Unbox(t, mod)
		if exists {
			return unboxUnit(inner, mod)
		} else {
			return false
		}
	}
}
func unboxTuple(t typsys.Type, mod string) (typsys.Tuple, bool) {
	var tuple, is_tuple = getTuple(t)
	if is_tuple {
		return tuple, true
	} else {
		var inner, _, exists = typsys.Unbox(t, mod)
		if exists {
			return unboxTuple(inner, mod)
		} else {
			return typsys.Tuple {}, false
		}
	}
}
func unboxRecord(t typsys.Type, mod string) (typsys.Record, bool) {
	var record, is_record = getRecord(t)
	if is_record {
		return record, true
	} else {
		var inner, _, exists = typsys.Unbox(t, mod)
		if exists {
			return unboxRecord(inner, mod)
		} else {
			return typsys.Record {}, false
		}
	}
}
func obtainIsoTuple(t typsys.Type, mod string) (typsys.Tuple, bool) {
	var tuple, is_tuple = getTuple(t)
	if is_tuple {
		return tuple, true
	} else {
		var inner, box, is_box = typsys.Unbox(t, mod)
		if is_box && box.BoxKind == typsys.Isomorphic {
			return obtainIsoTuple(inner, mod)
		} else {
			return typsys.Tuple {}, false
		}
	}
}
func obtainIsoRecord(t typsys.Type, mod string) (typsys.Record, bool) {
	var record, is_record = getRecord(t)
	if is_record {
		return record, true
	} else {
		var inner, box, is_box = typsys.Unbox(t, mod)
		if is_box && box.BoxKind == typsys.Isomorphic {
			return obtainIsoRecord(inner, mod)
		} else {
			return typsys.Record {}, false
		}
	}
}


